/*
Miranda IM for Windows Search

Copyright (C) 2012-2014 Nikolay Raspopov

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
*/

#include "stdafx.h"
#include "MirandaSearch.h"
#include "MirandaFilter.h"
#include "MirandaPLugin.h"
#include "Strings.h"

// CMirandaFilter

CMirandaFilter::CMirandaFilter()
	: m_nChunkID		( 0 )
	, m_nURLLevel		( URL_LEVEL_BAD )
	, m_bValueValid		( false )
{
}

HRESULT CMirandaFilter::FinalConstruct()
{
	return S_OK;
}

void CMirandaFilter::FinalRelease()
{
}

STDMETHODIMP CMirandaFilter::Init(const CString& sURL, const CString& sRelativeURL, URLLevel nLevel)
{
	m_sURL = sURL;
	m_sRelativeURL = URLDecode( sRelativeURL );
	m_nURLLevel = nLevel;

	return S_OK;
}

// IFilter

STDMETHODIMP CMirandaFilter::Init(/* [in] */ ULONG /*grfFlags*/, /* [in] */ ULONG /*cAttributes*/, /* [size_is][in] */ const FULLPROPSPEC* /*aAttributes*/, /* [out] */ ULONG* pFlags)
{
	*pFlags = 0;

	HRESULT hr = E_FAIL;

	switch ( m_nURLLevel )
	{
	case URL_LEVEL_CONTACTS:
		m_pFilter.Attach( new CContacts() );
		break;

	case URL_LEVEL_EVENTS:
		m_pFilter.Attach( new CEvents() );
		break;

	case URL_LEVEL_MESSAGE:
		m_pFilter.Attach( new CMessage() );
		break;
	}
	if ( m_pFilter )
	{
		m_pFilter->m_pOwner = this;
		hr = m_pFilter->Init( UTF8Encode( URLEncode( m_sRelativeURL ) ) );
	}

#ifdef _DEBUG
	if ( FAILED( hr ) )
		ATLTRACE( "0x%08x : CMirandaFilter : IFilter::Init() -> 0x%08x ( \"%hs\" )\n", GetCurrentThreadId(), hr, (LPCSTR)CT2A( (LPCTSTR)m_sRelativeURL ) );
#endif

	return hr;
}

STDMETHODIMP CMirandaFilter::GetChunk(/* [out] */ STAT_CHUNK *pStat)
{
	m_pvValue.Clear();
	m_bValueValid = false;

	if ( ! pStat )
	{
		ATLTRACE( "0x%08x : CMirandaFilter : IFilter::GetChunk() -> E_POINTER\n", GetCurrentThreadId() );
		return E_POINTER;
	}
	ZeroMemory( pStat, sizeof( STAT_CHUNK ) );

	if ( ! m_pFilter )
	{
		ATLTRACE( "0x%08x : CMirandaFilter : IFilter::GetChunk() -> FILTER_E_END_OF_CHUNKS\n", GetCurrentThreadId() );
		return FILTER_E_END_OF_CHUNKS;
	}

	pStat->idChunk = ++m_nChunkID;
	pStat->flags = CHUNK_VALUE;
	pStat->locale = GetUserDefaultLCID();

	HRESULT hr = m_pFilter->GetChunk( pStat, m_pvValue );
	if ( SUCCEEDED( hr ) )
	{
		m_bValueValid = true;
	}
#ifdef _DEBUG
	else if ( FILTER_E_END_OF_CHUNKS != hr )
		ATLTRACE( "0x%08x : CMirandaFilter : IFilter::GetChunk() -> 0x%08x\n", GetCurrentThreadId(), hr );
#endif

	return hr;
}

STDMETHODIMP CMirandaFilter::GetText(/* [out][in] */ ULONG* /*pcwcBuffer*/, /* [size_is][out] */ WCHAR* /*awcBuffer*/)
{
	ATLTRACE( "0x%08x : CMirandaFilter : IFilter::GetText() -> FILTER_E_NO_TEXT\n", GetCurrentThreadId() );

	return FILTER_E_NO_TEXT;
}

STDMETHODIMP CMirandaFilter::GetValue(/* [out] */ PROPVARIANT **ppPropValue)
{
	if ( ! ppPropValue )
	{
		ATLTRACE( "0x%08x : CMirandaFilter : IFilter::GetValue() -> E_POINTER\n", GetCurrentThreadId() );
		return E_POINTER;
	}
	*ppPropValue = NULL;

	// GetValue() must be called only once for every chunk
	if ( ! m_bValueValid )
	{
		ATLTRACE( "0x%08x : CMirandaFilter : IFilter::GetValue() -> FILTER_E_NO_VALUES( VT_EMPTY )\n", GetCurrentThreadId() );
		return FILTER_E_NO_VALUES;
	}
	m_bValueValid = false;

    CComHeapPtr< PROPVARIANT > sprop;
    HRESULT hr = E_OUTOFMEMORY;
    if ( sprop.Allocate() )
    {
        PropVariantInit( sprop );
        hr = m_pvValue.Detach( sprop );
        if ( SUCCEEDED( hr ) )
        {
            *ppPropValue = sprop.Detach(); 
        }
   }

#ifdef _DEBUG
	if ( FAILED( hr ) )
		ATLTRACE( "0x%08x : CMirandaFilter : IFilter::GetValue() -> 0x%08x\n", GetCurrentThreadId(), hr );
#endif

	return SUCCEEDED( hr ) ? FILTER_S_LAST_VALUES : FILTER_E_NO_VALUES;
}

STDMETHODIMP CMirandaFilter::BindRegion(/* [in] */ FILTERREGION /*origPos*/,/* [in] */ REFIID /*riid*/, /* [out] */ void** /*ppunk*/)
{
	ATLTRACE( "0x%08x : CMirandaFilter : IFilter::BindRegion() -> E_NOTIMPL\n", GetCurrentThreadId() );

	return E_NOTIMPL;
}

// CFilter

CFilter::CFilter()
	: m_pOwner				( NULL )
	, m_posPropertiesEnum	( NULL )
{
}

CFilter::~CFilter()
{
	for ( POSITION pos = m_nProperties.GetHeadPosition(); pos; )
		delete m_nProperties.GetNext( pos );
}

void CFilter::Add(const PROPERTYKEY& key)
{
	CAutoPtr< CItem > pItem( new CItem() );
	if ( ! pItem )
		return;

	pItem->m_Attribute.guidPropSet = key.fmtid;                                    
	pItem->m_Attribute.psProperty.lpwstr = NULL;
	pItem->m_Attribute.psProperty.ulKind = PRSPEC_PROPID;
	pItem->m_Attribute.psProperty.propid = key.pid;

	m_nProperties.AddTail( pItem.Detach() );

	m_posPropertiesEnum = m_nProperties.GetHeadPosition();
}

template< typename T >
void CFilter::Add(const PROPERTYKEY& key, const T& szValue)
{
	CAutoPtr< CItem > pItem( new CItem() );
	if ( ! pItem )
		return;

	pItem->m_Attribute.guidPropSet = key.fmtid;                                    
	pItem->m_Attribute.psProperty.lpwstr = NULL;
	pItem->m_Attribute.psProperty.ulKind = PRSPEC_PROPID;
	pItem->m_Attribute.psProperty.propid = key.pid;

	pItem->m_pvValue = szValue;

	m_nProperties.AddTail( pItem.Detach() );

	m_posPropertiesEnum = m_nProperties.GetHeadPosition();
}

void CFilter::AddUrl(LPCWSTR szURL, const FILETIME& ftTimestamp)
{
	CAutoPtr< CItem > pItem( new CItem() );
	if ( pItem )
	{
		CComHeapPtr< PROPVARIANT > spVector;
		if ( spVector.Allocate( 2 ) )
		{
			size_t nValSize = wcslen( szURL ) + 1;
	
			CComHeapPtr< WCHAR > spszVal;
			if ( spszVal.Allocate( nValSize ) )
			{
				wcscpy_s( (LPWSTR)spszVal, nValSize, szURL );

				pItem->m_Attribute.guidPropSet = PKEY_Search_UrlToIndexWithModificationTime.fmtid;
				pItem->m_Attribute.psProperty.ulKind = PRSPEC_PROPID;
				pItem->m_Attribute.psProperty.propid = PKEY_Search_UrlToIndexWithModificationTime.pid;

				pItem->m_pvValue.vt = VT_VARIANT | VT_VECTOR;
				pItem->m_pvValue.capropvar.cElems = 2;
				pItem->m_pvValue.capropvar.pElems = spVector.Detach();
				pItem->m_pvValue.capropvar.pElems[ 0 ].vt = VT_LPWSTR;
				pItem->m_pvValue.capropvar.pElems[ 0 ].pwszVal = spszVal.Detach();
				pItem->m_pvValue.capropvar.pElems[ 1 ].vt = VT_FILETIME;
				pItem->m_pvValue.capropvar.pElems[ 1 ].filetime = ftTimestamp;

				m_nProperties.AddTail( pItem.Detach() );
			}
		}
	}
	m_posPropertiesEnum = m_nProperties.GetHeadPosition();
}

void CFilter::AddVector(const PROPERTYKEY& key, size_t n, ...)
{
	va_list argptr;
	va_start( argptr, n );

	CAutoPtr< CItem > pItem( new CItem() );
	if ( pItem )
	{
		CComHeapPtr< LPWSTR > spVector;
		if ( spVector.Allocate( n ) )
		{
			CAutoVectorPtr< CComHeapPtr< WCHAR > > spszVal( new CComHeapPtr< WCHAR >[ n ] );
			size_t i;
			for ( i = 0; i < n; ++i )
			{
				LPCWSTR szValue = va_arg( argptr, LPCWSTR );

				size_t nValSize = wcslen( szValue ) + 1;
				
				if ( ! spszVal[ i ].Allocate( nValSize ) )
					break;

				wcscpy_s( (LPWSTR)spszVal[ i ], nValSize, szValue );
			}

			if ( i == n )
			{
				pItem->m_Attribute.guidPropSet = key.fmtid;                                    
				pItem->m_Attribute.psProperty.lpwstr = NULL;
				pItem->m_Attribute.psProperty.ulKind = PRSPEC_PROPID;
				pItem->m_Attribute.psProperty.propid = key.pid;

				pItem->m_pvValue.vt = VT_LPWSTR | VT_VECTOR;
				pItem->m_pvValue.calpwstr.cElems = (ULONG)n;
				pItem->m_pvValue.calpwstr.pElems = spVector.Detach();
				for ( i = 0; i < n; ++i )
					pItem->m_pvValue.calpwstr.pElems[ i ] = spszVal[ i ].Detach();

				m_nProperties.AddTail( pItem.Detach() );
			}
		}
	}
	m_posPropertiesEnum = m_nProperties.GetHeadPosition();

	va_end( argptr );
}

STDMETHODIMP CFilter::GetChunk(STAT_CHUNK* pStat, CComPropVariant& vValue)
{
	if ( ! m_posPropertiesEnum )
		return FILTER_E_END_OF_CHUNKS;

	CItem* pItem = m_nProperties.GetNext( m_posPropertiesEnum );

	pStat->attribute = pItem->m_Attribute;
	vValue = pItem->m_pvValue;

	return S_OK;
}

// CContacts

STDMETHODIMP CContacts::Init(LPCSTR szURL)
{
	// Packet format:
	// < < last event time : time_t >< zero terminated contact name : WCHAR[] > >[]

	CAtlArray< BYTE > pData;
	if ( ! _AtlModule.ReadData( szURL, pData ) )
	{
		ATLTRACE( "CContacts::Init() : Failed to get contact list.\n" );
		return FILTER_E_ACCESS;
	}

	AddVector( PKEY_Kind, 2, KIND_INSTANTMESSAGE, KIND_COMMUNICATION );
	Add( PKEY_ItemType, TYPE );
	Add( PKEY_ParsingPath, (LPCWSTR)m_pOwner->GetURL() );
	Add( PKEY_ItemUrl, (LPCWSTR)m_pOwner->GetURL() );
	Add( PKEY_ItemNameDisplay, TITLE );
	AddVector( PKEY_Shell_SFGAOFlagsStrings, 2, SFGAOSTR_SUPERHIDDEN, SFGAOSTR_HIDDEN );

	size_t nSize = pData.GetCount();
	const BYTE* pBuf = (const BYTE*)pData.GetData();	

	ULONGLONG nTimestampMax = 0,  nTimestampMin = (ULONGLONG)-1;
	while ( nSize > sizeof( ListData ) + sizeof( WCHAR ) )
	{
		const ListData& data = *(const ListData*)pBuf;

		// TODO: Add data.wszName length strict check

		CString sURL = m_pOwner->GetURL() + URLEncode( data.wszName ) + _T("/");

		// time_t -> FILETIME
		ULONGLONG nTimestamp = ( (ULONGLONG)data.tTimestamp + 11644473600ui64 ) * 10000000ui64;
		FILETIME ftTimestamp = { (DWORD)nTimestamp, (DWORD)( nTimestamp >> 32 ) };
	
		ATLTRACE( "CContacts::Init() : Found contact \"%s\" [%s]\n", (LPCSTR)CW2A( data.wszName ), (LPCSTR)CT2A( COleDateTime( ftTimestamp ).Format( _T("%c") ) ) );

		AddUrl( sURL, ftTimestamp );
	
		if ( nTimestamp > nTimestampMax )
			nTimestampMax = nTimestamp;
		if ( nTimestamp < nTimestampMin )
			nTimestampMin = nTimestamp;

		size_t nLen = sizeof( ListData ) + ( wcslen( data.wszName ) + 1 ) * sizeof( WCHAR );
		pBuf += nLen;
		nSize -= nLen;
	}

	if ( nTimestampMax )
	{
		FILETIME ftDateCreated = { (DWORD)nTimestampMin, (DWORD)( nTimestampMin >> 32 ) };
		Add( PKEY_DateCreated, ftDateCreated);
		FILETIME ftDateModified = { (DWORD)nTimestampMax, (DWORD)( nTimestampMax >> 32 ) };
		Add( PKEY_DateModified, ftDateModified );
		FILETIME ftDateAccessed = { (DWORD)nTimestampMax, (DWORD)( nTimestampMax >> 32 ) };
		Add( PKEY_DateAccessed, ftDateAccessed );
	}

	if ( nSize )
		ATLTRACE( "CContacts::Init() : Contact list has invalid format.\n" );

	return S_OK;
}

// CEvents

STDMETHODIMP CEvents::Init(LPCSTR szURL)
{
	// Packet format:
	// < event time : time_t >[]

	CStringA sRelativeURL = szURL;
	CString sName = URLDecode( sRelativeURL.Mid( 1 ).SpanExcluding( "/" ) );

	CAtlArray< BYTE > pData;
	if ( ! _AtlModule.ReadData( szURL, pData ) )
	{
		ATLTRACE( "CEvents::Init( \"%s\" ) : Failed to get event list of contact\n", szURL );
		return FILTER_E_ACCESS;
	}
	else if ( pData.GetCount() % sizeof( time_t ) != 0 )
	{
		ATLTRACE( "CEvents::Init( \"%s\" ) : Event list of contact has invalid format.\n", szURL );
		return FILTER_E_ACCESS;
	}

	AddVector( PKEY_Kind, 2, KIND_INSTANTMESSAGE, KIND_COMMUNICATION );
	Add( PKEY_ItemType, TYPE );
	Add( PKEY_ParsingPath, (LPCWSTR)m_pOwner->GetURL() );
	Add( PKEY_ItemUrl, (LPCWSTR)m_pOwner->GetURL() );
	Add( PKEY_ItemNameDisplay, (LPCWSTR)sName );
	AddVector( PKEY_Shell_SFGAOFlagsStrings, 2, SFGAOSTR_SUPERHIDDEN, SFGAOSTR_HIDDEN );

	int nEventCount = (int)( pData.GetCount() / sizeof( time_t ) );
	const time_t* tTimestamp = (const time_t*)pData.GetData();

	ULONGLONG nTimestampMax = 0,  nTimestampMin = (ULONGLONG)-1;
	for ( int i = 0; i < nEventCount; ++i )
	{
		CString sURL;
		sURL.Format( _T("%s%d"), (LPCTSTR)m_pOwner->GetURL(), i + 1 ); // begins from 1

		// time_t -> FILETIME
		ULONGLONG nTimestamp = ( (ULONGLONG)tTimestamp[ i ] + 11644473600ui64 ) * 10000000ui64;
		FILETIME ftTimestamp = { (DWORD)nTimestamp, (DWORD)( nTimestamp >> 32 ) };

		AddUrl( sURL, ftTimestamp );

		if ( nTimestamp > nTimestampMax )
			nTimestampMax = nTimestamp;
		if ( nTimestamp < nTimestampMin )
			nTimestampMin = nTimestamp;
	}

	if ( nTimestampMax )
	{
		FILETIME ftDateCreated = { (DWORD)nTimestampMin, (DWORD)( nTimestampMin >> 32 ) };
		Add( PKEY_DateCreated, ftDateCreated);
		FILETIME ftDateModified = { (DWORD)nTimestampMax, (DWORD)( nTimestampMax >> 32 ) };
		Add( PKEY_DateModified, ftDateModified );
		FILETIME ftDateAccessed = { (DWORD)nTimestampMax, (DWORD)( nTimestampMax >> 32 ) };
		Add( PKEY_DateAccessed, ftDateAccessed );

		ATLTRACE( "CEvents::Init() : Found %u events in \"%s\" [%s]\n", nEventCount, (LPCSTR)CT2A( sName ), (LPCSTR)CT2A( COleDateTime( ftDateModified ).Format( _T("%c") ) ) );
	}
	else
		ATLTRACE( "CEvents::Init() : Found no events in \"%s\"\n", (LPCSTR)CT2A( sName ) );

	return S_OK;
}

// CMessage

STDMETHODIMP CMessage::Init(LPCSTR szURL)
{
	// Packet format:
	// < flags : DWORD >< time : time_t >< zero terminated text : WCHAR[] >

	CStringA sRelativeURL = szURL;
	CString sName = URLDecode( sRelativeURL.Mid( 1 ).SpanExcluding( "/" ) );
	//CString sNumber = URLDecode( sRelativeURL.Mid( sRelativeURL.Find( '/' ) + 1 ) );

	CAtlArray< BYTE > pData;
	if ( ! _AtlModule.ReadData( szURL, pData ) )
	{
		ATLTRACE( "CMessage::Init( \"%s\" ) : Failed to get message\n", szURL );
		return FILTER_E_ACCESS;
	}
	if ( pData.GetCount() <= sizeof( EventData )  )
	{
		ATLTRACE( "CMessage::Init( \"%s\" ) : Message has invalid format, got %u bytes (must be > %u bytes)\n", szURL, pData.GetCount(), sizeof( EventData ) );
		return FILTER_E_ACCESS;
	}
	if ( ( pData.GetCount() - sizeof( EventData ) ) % sizeof( WCHAR ) != 0 )
	{
		ATLTRACE( "CMessage::Init( \"%s\" ) : Message has invalid format, got %u bytes (must be even)\n", szURL, pData.GetCount() - sizeof( EventData ) );
		return FILTER_E_ACCESS;
	}

	const EventData& data = *(const EventData*)pData.GetData();
	size_t nTextLength = ( pData.GetCount() - sizeof( EventData ) ) / sizeof( WCHAR );

	// time_t -> FILETIME
	ULONGLONG nTimestamp = ( (ULONGLONG)data.tTimestamp + 11644473600ui64 ) * 10000000ui64;
	FILETIME ftTimestamp = { (DWORD)nTimestamp, (DWORD)( nTimestamp >> 32 ) };

	CString sMessage( data.wszText, (int)nTextLength );

	CString sSummary = sMessage;
	sSummary.Replace( _T('\r'), _T(' ') );
	sSummary.Replace( _T('\n'), _T(' ') );
	sSummary.Replace( _T('\t'), _T(' ') );
	while( sSummary.Replace( _T("  "), _T(" ") ) > 0 );
	sSummary.Trim();
	if ( sSummary.IsEmpty() )
	{
		ATLTRACE( "CMessage::Init( \"%s\" ) : Message is empty\n", szURL );
		return FILTER_E_END_OF_CHUNKS;
	}
	if ( sSummary.GetLength() > 250 )
		sSummary = sSummary.Left( 250 ) + _T("...");
	sSummary = sSummary.Left( 1 ).MakeUpper() + sSummary.Mid( 1 );

	CString sFrom = ( data.nFlags & DBEF_SENT ) ? _AtlModule.GetMe() : sName;
	CString sTo = ( data.nFlags & DBEF_SENT ) ? sName : _AtlModule.GetMe();
	CString sID = TITLE _T(" - ") + _AtlModule.GetMe() + _T(" - ") + sName;

	// Default properties:
	// System.ItemType (PKEY_ItemType) 
	// System.ParsingPath (PKEY_ParsingPath) 
	// System.ItemPathDisplay (PKEY_ItemPathDisplay) 
	// System.ItemNameDisplay (PKEY_ItemNameDisplay) 

	AddVector( PKEY_Kind, 2, KIND_INSTANTMESSAGE, KIND_COMMUNICATION );
	Add( PKEY_ItemType, TYPE );
	Add( PKEY_ParsingPath, (LPCWSTR)m_pOwner->GetURL() );
	Add( PKEY_ItemUrl, (LPCWSTR)m_pOwner->GetURL() );
	//Add( PKEY_ParsingName );
	Add( PKEY_ItemPathDisplay, (LPCWSTR)sID );
	Add( PKEY_ItemNameDisplay, (LPCWSTR)sSummary );
	//Add( PKEY_ItemName );
	//Add( PKEY_ItemFolderNameDisplay );
	//Add( PKEY_ItemFolderPathDisplay );

	Add( PKEY_DateCreated, ftTimestamp );
	Add( PKEY_DateModified, ftTimestamp );
	Add( PKEY_DateAccessed, ftTimestamp );
	Add( PKEY_Title, (LPCWSTR)sSummary );
	AddVector( PKEY_Author, 2, (LPCWSTR)sFrom, (LPCWSTR)sTo );
	Add( PKEY_Message_ConversationID, (LPCWSTR)sID );

	Add( PKEY_Subject, (LPCWSTR)sSummary );
	Add( PKEY_Message_FromName, (LPCWSTR)sFrom );
	Add( PKEY_Message_ToName, (LPCWSTR)sTo );
	Add( PKEY_Message_DateSent, ftTimestamp );
	Add( PKEY_Message_DateReceived, ftTimestamp );
	Add( PKEY_Size, (ULONGLONG)sMessage.GetLength() );
	Add( PKEY_Search_AutoSummary, (LPCWSTR)sMessage );
	Add( PKEY_Search_Contents, (LPCWSTR)sMessage );

	//Add( PKEY_PerceivedType, (LONG)PERCEIVED_TYPE_TEXT );
	//Add( PKEY_MIMEType, L"text/plain" );
	//Add( PKEY_ContentType, L"text/plain" );

	return S_OK;
}
